﻿///////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2023, ООО 1С-Софт
// Все права защищены. Эта программа и сопроводительные материалы предоставляются 
// в соответствии с условиями лицензии Attribution 4.0 International (CC BY 4.0)
// Текст лицензии доступен по ссылке:
// https://creativecommons.org/licenses/by/4.0/legalcode
///////////////////////////////////////////////////////////////////////////////////////////////////////

#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда

#Область ОписаниеПеременных

Перем ОшибкаРасчетаКурсаПоФормуле;
Перем КодыВалют;

#КонецОбласти

#Область ОбработчикиСобытий

// При записи контролируются курсы подчиненных валют.
//
Процедура ПриЗаписи(Отказ, Замещение)
	
	Если ОбменДанными.Загрузка Тогда
		Возврат;
	КонецЕсли;
	
	Если ДополнительныеСвойства.Свойство("ОтключитьКонтрольПодчиненныхВалют") Тогда
		Возврат;
	КонецЕсли;
		
	ДополнительныеСвойства.Вставить("ЗависимыеВалюты", Новый Соответствие);
	
	Если Количество() > 0 Тогда
		ОбновитьКурсыПодчиненныхВалют();
	Иначе
		УдалитьКурсыПодчиненныхВалют();
	КонецЕсли;
	
КонецПроцедуры

#КонецОбласти

#Область СлужебныеПроцедурыИФункции

// Находит все зависимые валюты и изменяет их курс.
//
Процедура ОбновитьКурсыПодчиненныхВалют()
	
	ВыбраннаяВалюта = Неопределено;
	ДополнительныеСвойства.Свойство("ОбновитьКурсЗависимойВалюты", ВыбраннаяВалюта);
	
	Для Каждого ЗаписьОсновнойВалюты Из ЭтотОбъект Цикл
	
		Если ВыбраннаяВалюта <> Неопределено Тогда // Нужно обновить курс только указанной валюты.
			ЗаблокироватьКурсЗависимойВалюты(ВыбраннаяВалюта, ЗаписьОсновнойВалюты.Период); 
		Иначе
			ЗависимыеВалюты = РаботаСКурсамиВалют.СписокЗависимыхВалют(ЗаписьОсновнойВалюты.Валюта, ДополнительныеСвойства);
			Для Каждого ЗависимаяВалюта Из ЗависимыеВалюты Цикл
				ЗаблокироватьКурсЗависимойВалюты(ЗависимаяВалюта, ЗаписьОсновнойВалюты.Период); 
			КонецЦикла;
		КонецЕсли;
		
	КонецЦикла;
	
	Для Каждого ЗаписьОсновнойВалюты Из ЭтотОбъект Цикл

		Если ВыбраннаяВалюта <> Неопределено Тогда // Нужно обновить курс только указанной валюты.
			ОбновленныеПериоды = Неопределено;
			Если Не ДополнительныеСвойства.Свойство("ОбновленныеПериоды", ОбновленныеПериоды) Тогда
				ОбновленныеПериоды = Новый Соответствие;
				ДополнительныеСвойства.Вставить("ОбновленныеПериоды", ОбновленныеПериоды);
			КонецЕсли;
			// Повторно не обновляем курс за один и тот же период.
			Если ОбновленныеПериоды[ЗаписьОсновнойВалюты.Период] = Неопределено Тогда
				ОбновитьКурсЗависимойВалюты(ВыбраннаяВалюта, ЗаписьОсновнойВалюты); 
				ОбновленныеПериоды.Вставить(ЗаписьОсновнойВалюты.Период, Истина);
			КонецЕсли;
		Иначе	// Обновить курс всех зависимых валют.
			ЗависимыеВалюты = РаботаСКурсамиВалют.СписокЗависимыхВалют(ЗаписьОсновнойВалюты.Валюта, ДополнительныеСвойства);
			Для Каждого ЗависимаяВалюта Из ЗависимыеВалюты Цикл
				ОбновитьКурсЗависимойВалюты(ЗависимаяВалюта, ЗаписьОсновнойВалюты); 
			КонецЦикла;
		КонецЕсли;
		
	КонецЦикла;
	
КонецПроцедуры

Процедура ЗаблокироватьКурсЗависимойВалюты(ЗависимаяВалюта, ПериодОсновнойВалюты)
	
	Блокировка = Новый БлокировкаДанных;
	ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.КурсыВалют");
	ЭлементБлокировки.УстановитьЗначение("Валюта", ЗависимаяВалюта.Ссылка);
	Если ЗначениеЗаполнено(ПериодОсновнойВалюты) Тогда
		ЭлементБлокировки.УстановитьЗначение("Период", ПериодОсновнойВалюты);
	КонецЕсли;
	Блокировка.Заблокировать();
	
КонецПроцедуры
	
Процедура ОбновитьКурсЗависимойВалюты(ЗависимаяВалюта, ЗаписьОсновнойВалюты)
	
	НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
	НаборЗаписей.Отбор.Валюта.Установить(ЗависимаяВалюта.Ссылка, Истина);
	НаборЗаписей.Отбор.Период.Установить(ЗаписьОсновнойВалюты.Период, Истина);
	
	ЗаписьКурсовВалют = НаборЗаписей.Добавить();
	ЗаписьКурсовВалют.Валюта = ЗависимаяВалюта.Ссылка;
	ЗаписьКурсовВалют.Период = ЗаписьОсновнойВалюты.Период;
	Если ЗависимаяВалюта.СпособУстановкиКурса = Перечисления.СпособыУстановкиКурсаВалюты.НаценкаНаКурсДругойВалюты Тогда
		ЗаписьКурсовВалют.Курс = ЗаписьОсновнойВалюты.Курс + ЗаписьОсновнойВалюты.Курс * ЗависимаяВалюта.Наценка / 100;
		ЗаписьКурсовВалют.Кратность = ЗаписьОсновнойВалюты.Кратность;
	Иначе // по формуле
		Курс = КурсВалютыПоФормуле(ЗависимаяВалюта.Ссылка, ЗависимаяВалюта.ФормулаРасчетаКурса, ЗаписьОсновнойВалюты.Период);
		Если Курс <> Неопределено Тогда
			ЗаписьКурсовВалют.Курс = Курс;
			ЗаписьКурсовВалют.Кратность = 1;
		КонецЕсли;
	КонецЕсли;
		
	Если ЗаписьКурсовВалют.Курс = 0 Тогда
		Возврат;
	КонецЕсли;
	
	НаборЗаписей.ДополнительныеСвойства.Вставить("ОтключитьКонтрольПодчиненныхВалют");
	НаборЗаписей.ДополнительныеСвойства.Вставить("ПропуститьПроверкуЗапретаИзменения");
	
	Блокировка = Новый БлокировкаДанных;
	ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.КурсыВалют");
	ЭлементБлокировки.УстановитьЗначение("Валюта", ЗаписьКурсовВалют.Валюта);
	ЭлементБлокировки.УстановитьЗначение("Период", ЗаписьКурсовВалют.Период);
	
	НачатьТранзакцию();
	Попытка
		Блокировка.Заблокировать();
		НаборЗаписей.Записать();
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		ВызватьИсключение;
	КонецПопытки;
	
КонецПроцедуры

// Очищает курсы зависимых валют.
//
Процедура УдалитьКурсыПодчиненныхВалют()
	
	ВалютаВладелец = Отбор.Валюта.Значение;
	Период = Отбор.Период.Значение;
	
	ЗависимаяВалюта = Неопределено;
	Если ДополнительныеСвойства.Свойство("ОбновитьКурсЗависимойВалюты", ЗависимаяВалюта) Тогда
		ЗаблокироватьКурсЗависимойВалюты(ЗависимаяВалюта, Период);
		УдалитьКурсыВалюты(ЗависимаяВалюта, Период);
	Иначе
		ЗависимыеВалюты = РаботаСКурсамиВалют.СписокЗависимыхВалют(ВалютаВладелец, ДополнительныеСвойства);
		Для Каждого ЗависимаяВалюта Из ЗависимыеВалюты Цикл
			ЗаблокироватьКурсЗависимойВалюты(ЗависимаяВалюта.Ссылка, Период); 
		КонецЦикла;
		Для Каждого ЗависимаяВалюта Из ЗависимыеВалюты Цикл
			УдалитьКурсыВалюты(ЗависимаяВалюта.Ссылка, Период);
		КонецЦикла;
	КонецЕсли;
	
КонецПроцедуры

Процедура УдалитьКурсыВалюты(ВалютаСсылка, Период)
	НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
	НаборЗаписей.Отбор.Валюта.Установить(ВалютаСсылка);
	НаборЗаписей.Отбор.Период.Установить(Период);
	НаборЗаписей.ДополнительныеСвойства.Вставить("ОтключитьКонтрольПодчиненныхВалют");
	НаборЗаписей.Записать();
КонецПроцедуры
	
Функция КурсВалютыПоФормуле(Валюта, Формула, Период)
	
	Если КодыВалют = Неопределено Тогда
		КодыВалют = КодыВалют();
	КонецЕсли;
	
	ТекстЗапроса = 
	"ВЫБРАТЬ
	|	Валюты.Ссылка КАК Ссылка,
	|	Валюты.СимвольныйКод КАК СимвольныйКод
	|ПОМЕСТИТЬ Валюты
	|ИЗ
	|	&КодыВалют КАК Валюты
	|;
	|
	|////////////////////////////////////////////////////////////////////////////////
	|ВЫБРАТЬ
	|	Валюты.СимвольныйКод КАК СимвольныйКод,
	|	ЕСТЬNULL(КурсыВалютСрезПоследних.Курс, 1) / ЕСТЬNULL(КурсыВалютСрезПоследних.Кратность, 1) КАК Курс
	|ИЗ
	|	Валюты КАК Валюты
	|		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют.СрезПоследних(&Период, ) КАК КурсыВалютСрезПоследних
	|		ПО (КурсыВалютСрезПоследних.Валюта = Валюты.Ссылка)";
	
	Запрос = Новый Запрос(ТекстЗапроса);
	Запрос.УстановитьПараметр("КодыВалют", КодыВалют);
	Запрос.УстановитьПараметр("Период", Период);

	Выражение = ФорматироватьЧисла(Формула);
	
	Выборка = Запрос.Выполнить().Выбрать();
	Пока Выборка.Следующий() Цикл
		Выражение = СтрЗаменить(Выражение, Выборка.СимвольныйКод, Формат(Выборка.Курс, "ЧРД=.; ЧГ=0"));
	КонецЦикла;
	
	Попытка
		Результат = ОбщегоНазначения.ВычислитьВБезопасномРежиме(Выражение);
	Исключение
		Если ОшибкаРасчетаКурсаПоФормуле = Неопределено Тогда
			ОшибкаРасчетаКурсаПоФормуле = Новый Соответствие;
		КонецЕсли;
		Если ОшибкаРасчетаКурсаПоФормуле[Валюта] = Неопределено Тогда
			ОшибкаРасчетаКурсаПоФормуле.Вставить(Валюта, Истина);
			ИнформацияОбОшибке = ИнформацияОбОшибке();
			
			ТекстОшибки = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(НСтр("ru = 'Расчет курса валюты ""%1"" по формуле ""%2"" за период ""%3""не выполнен:'",
				ОбщегоНазначения.КодОсновногоЯзыка()), Валюта, Формула, Период);
				
			ОбщегоНазначения.СообщитьПользователю(ТекстОшибки + Символы.ПС + ОбработкаОшибок.КраткоеПредставлениеОшибки(ИнформацияОбОшибке), 
				Валюта, "Объект.ФормулаРасчетаКурса");
				
			Если ДополнительныеСвойства.Свойство("ОбновитьКурсЗависимойВалюты") Тогда
				ВызватьИсключение ТекстОшибки + Символы.ПС + ОбработкаОшибок.КраткоеПредставлениеОшибки(ИнформацияОбОшибке);
			КонецЕсли;
			
			ЗаписьЖурналаРегистрации(НСтр("ru = 'Валюты.Загрузка курсов валют'", ОбщегоНазначения.КодОсновногоЯзыка()),
				УровеньЖурналаРегистрации.Ошибка, Валюта.Метаданные(), Валюта, 
				ТекстОшибки + Символы.ПС + ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке));
		КонецЕсли;
		Результат = Неопределено;
	КонецПопытки;
	
	Возврат Результат;
КонецФункции

Функция КодыВалют()
	
	КодыВалют = Неопределено;
	ДополнительныеСвойства.Свойство("КодыВалют", КодыВалют);
	Если КодыВалют = Неопределено Тогда
		КодыВалют = Справочники.Валюты.КодыВалют();
	КонецЕсли;
	
	Результат = Новый ТаблицаЗначений;
	Результат.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("СправочникСсылка.Валюты"));
	Результат.Колонки.Добавить("СимвольныйКод", Новый ОписаниеТипов("Строка", , , , Новый КвалификаторыСтроки(Метаданные.Справочники.Валюты.ДлинаНаименования, ДопустимаяДлина.Переменная)));
	
	Для Каждого ОписаниеВалюты Из КодыВалют Цикл
		// Валюта участвует в формуле, если символьный код содержит буквы.
		Если ЗначениеЗаполнено(СтрСоединить(СтрРазделить(ОписаниеВалюты.СимвольныйКод, "0123456789", Ложь), "")) Тогда
			ЗаполнитьЗначенияСвойств(Результат.Добавить(), ОписаниеВалюты);
		КонецЕсли;
	КонецЦикла;
	
	Результат.Индексы.Добавить("Ссылка");
	
	Возврат Результат;
	
КонецФункции

Функция ФорматироватьЧисла(Строка)
	
	Результат = "";
	Число = "";
	ЕстьРазделительВЧисле = Ложь;
	ПредыдущийСимвол = "";
	
	ДлинаСтроки = СтрДлина(Строка);
	Для Индекс = 1 По ДлинаСтроки Цикл
		Если Индекс < ДлинаСтроки Тогда
			СледующийСимвол = Сред(Строка, Индекс + 1, 1);
		Иначе
			СледующийСимвол = "";
		КонецЕсли;
		Символ = Сред(Строка, Индекс, 1);
		
		ПредыдущийСимволЭтоРазделитель = ПредыдущийСимвол = "" Или СтрНайти("()[]/*-+%=<>, ", ПредыдущийСимвол) > 0;
		
		Если ЭтоЦифра(Символ) И (ПредыдущийСимволЭтоРазделитель Или ЭтоЦифра(ПредыдущийСимвол) И ЗначениеЗаполнено(Число)) Тогда
			Число = Число + Символ;
		ИначеЕсли Не ЕстьРазделительВЧисле И (Символ = "," Или Символ = ".") И ЭтоЦифра(СледующийСимвол)
			И (ЭтоЦифра(ПредыдущийСимвол) Или ПредыдущийСимволЭтоРазделитель) И ЗначениеЗаполнено(Число) Тогда
			Число = Число + ".";
			ЕстьРазделительВЧисле = Истина;
		Иначе
			Результат = Результат + Число + Символ;
			Число = "";
			ЕстьРазделительВЧисле = Ложь;
		КонецЕсли;
		
		ПредыдущийСимвол = Символ;
		Символ = "";
	КонецЦикла;
	
	Результат = Результат + Число + Символ;
	Возврат Результат;
	
КонецФункции

Функция ЭтоЦифра(Символ)
	
	Возврат СтрНайти("1234567890", Символ) > 0;
	
КонецФункции


#КонецОбласти

#Иначе
ВызватьИсключение НСтр("ru = 'Недопустимый вызов объекта на клиенте.'");
#КонецЕсли